package generator;

import java.io.File;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;

import model.Model;

import org.w3c.dom.Attr;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

/***
 * Class for create and XMI file with a model
 * @author Federico
 *
 */
public class XmiGenerator extends Generator{
	
	DocumentBuilder doc;
	Document root;
	
	//Array for saving the class in the position of its id in the XMI file
	ArrayList<model.Class> idList = new ArrayList<model.Class>();
	
	int id = 0; //id for classes
	int anotherId = 0; //id for methods and attributes
	int generalizationId = 0; //if for generalizations
	
	final static String ATTRIBUTES = "UML:Attribute";
	final static String MODEL = "UML:Model";
	final static String NAMESPACE = "UML:Namespace.ownedElement";
	final static String CLASS = "UML:Class";
    final static String METHODS = "UML:Operation";
    final static String PARAMS = "UML:Parameter";
    final static String CLASS_DOWN = "UML:Classifier.feature";
    final static String METHODS_PARAMS = "UML:BehavioralFeature.parameter";
    final static String COUPLINGS = "UML:AssociationEnd";
    final static String CLASS_INHERITANCES = "UML:GeneralizableElement.generalization";
    final static String GENERALIZATION = "UML:Generalization";
    final static String FILE_ADDRESS = "test.xmi";
    
    //Path of the binary file, the same for the XMI to save there.
    String path;
	
	public XmiGenerator(Model m, String d) {
		super(m);
		path = d;
	}

	public boolean generate() {
		try {
			doc = DocumentBuilderFactory.newInstance().newDocumentBuilder();
			root = doc.newDocument();
			
			doCreate();
			
            //write the content into xmi file
            TransformerFactory transformerFactory = TransformerFactory.newInstance();
            Transformer transformer = transformerFactory.newTransformer();
            transformer.setOutputProperty(OutputKeys.INDENT, "yes");
            DOMSource source = new DOMSource(root);
            Calendar cal = Calendar.getInstance();
            int day = cal.get(Calendar.DATE);
            int month = cal.get(Calendar.MONTH) + 1;
            int year = cal.get(Calendar.YEAR);
            int i=0;
            
            File f = new File(path+"/"+"./generated_"+year + "_" + month + "_" + day+".xmi");
            while(f.exists()){
            	f = new File(path+"/"+"./generated_"+year + "_" + month + "_" + day+"("+i+").xmi");
                i++;
            }
            StreamResult result =  new StreamResult(f);
            transformer.transform(source, result);
 
        }catch(ParserConfigurationException ex){
            Logger.getLogger(XmiGenerator.class.getName()).log(Level.SEVERE, null, ex);
        }catch(TransformerException ex){
            Logger.getLogger(XmiGenerator.class.getName()).log(Level.SEVERE, null, ex);
        }
		return true;
	}

	/***
	 * Method which creates the XMI structure
	 */
	private void doCreate() {
		//Xmi
		Element rootXMI = root.createElement("XMI");
		root.appendChild(rootXMI);
		putAttribute(rootXMI,"xmlns:UML","org.omg.xmi.namespace.UML");
		putAttribute(rootXMI,"xmi.version","1.2");
		
		//Header for umbrello's compatibility
		injectHeader(rootXMI);
		
		Element childContent = root.createElement("XMI.content");
		rootXMI.appendChild(childContent);
		
		Element modelChild = root.createElement(MODEL);
		putAttribute(modelChild,"xmi.id","model"+id);
		putAttribute(modelChild,"name","Modelo");
		
		childContent.appendChild(modelChild);
		
		Element namespaceChild = root.createElement(NAMESPACE);
		modelChild.appendChild(namespaceChild);
		
		createClasses(namespaceChild);
	}
	
	/***
	 * method for creating the header to be compatible with Umbrello
	 * @param e Node of the parent
	 */
	private void injectHeader(Element e){
		Element childHeader = root.createElement("XMI.header");
		e.appendChild(childHeader);
		Element childDoc = root.createElement("XMI.documentation");
		childHeader.appendChild(childDoc);
		Element childExporter = root.createElement("XMI.exporter");
		childExporter.appendChild(root.createTextNode("umbrello uml modeller http://uml.sf.net"));
		childDoc.appendChild(childExporter);
		Element childExporterVersion = root.createElement("XMI.exporterVersion");
		childExporterVersion.appendChild(root.createTextNode("1.6.1"));
		childDoc.appendChild(childExporterVersion);
		Element childExporterEncoding = root.createElement("XMI.exporterEncoding");
		childExporterEncoding.appendChild(root.createTextNode("UnicodeUTF8"));
		childDoc.appendChild(childExporterEncoding);
		
		Element childMeta = root.createElement("XMI.metamodel");
		putAttribute(childMeta,"xmi.version","1.3");
		putAttribute(childMeta,"href","UML.xml");
		putAttribute(childMeta,"xmi.name","UML");
		childHeader.appendChild(childExporter);
	}
	
	/***
	 * Method for creating the classes tags and values
	 * @param e element in the tree, parent of the class node
	 */
	private void createClasses(Element e){
		Element classChild;
		for(model.Class c : model.getClasses()){
			classChild = root.createElement(CLASS);
			putAttribute(classChild, "xmi.id", ""+id);
			idList.add(id, c);
			id++;
			putAttribute(classChild, "name", c.getName());
			putAttribute(classChild, "visibility", "public");
			
			//Generalizations
			if(getParentId(c.getParent())!=-1)
				createAssociation(classChild);
			
			Element classifierChild = root.createElement(CLASS_DOWN);
			classChild.appendChild(classifierChild);
			
			//Check attributes and add if it's the case
			if(!c.getAttributes().isEmpty()){
				createAttributes(classifierChild, c);
			}
			
			//Check methodes and add if it's the case
			if(!c.getMethods().isEmpty()){
				createMethods(classifierChild, c);
			}
			
			e.appendChild(classChild);
			if(getParentId(c.getParent())!=-1)
				createGeneralization(e);
		}
	}
	
	
	/***
	 * Method for creating attributes to a class (c) with values and access information
	 * @param e Element of the class node
	 * @param c Class we want to add attributes
	 */
	private void createAttributes(Element e, model.Class c){
		Element attributeChild;
		String access;
		for(model.Attribute a : c.getAttributes()){
			attributeChild = root.createElement(ATTRIBUTES);
			putAttribute(attributeChild, "xmi.id", "attr"+anotherId);
			anotherId++;
			putAttribute(attributeChild, "name", a.getName());
			if(a.isPublic()) access = "public";
			else access = "private";
			putAttribute(attributeChild, "visibility", access);
			e.appendChild(attributeChild);
		}
	}
	
	/***
	 * Method for creating methods (operations) to a class(c)
	 * @param e Element of the class node
	 * @param c Class we want to add methods
	 */
	private void createMethods(Element e, model.Class c){
		Element operationChild, behavioralChild, parameterChild;
		String access;
		for(model.Method m : c.getMethods()){
			operationChild = root.createElement(METHODS);
			e.appendChild(operationChild);
			putAttribute(operationChild, "xmi.id", "meth"+anotherId);
			anotherId++;
			putAttribute(operationChild, "name", m.getName());
			if(m.isPublic()) access = "public";
			else access = "private";
			putAttribute(operationChild, "visibility", access);
			
			behavioralChild = root.createElement(METHODS_PARAMS);
			operationChild.appendChild(behavioralChild);
			for(model.Attribute p : m.getParameters()){
				parameterChild = root.createElement(PARAMS);
				putAttribute(parameterChild, "xmi.id", "parameter"+anotherId);
				anotherId++;
				putAttribute(parameterChild, "name", p.getName());
				behavioralChild.appendChild(parameterChild);
			}
		}
	}
	
	/***
	 * Method for establishing an association between classes
	 * @param e Element of the parent node
	 */
	private void createAssociation(Element e){
		Element generalizableChild = root.createElement(CLASS_INHERITANCES);
		e.appendChild(generalizableChild);
		Element generalization = root.createElement(GENERALIZATION);
		String idAssoc = "g"+generalizationId;
		putAttribute(generalization, "xmi.idref", idAssoc);
		generalizableChild.appendChild(generalization);
	}	
	
	/***
	 * Method for creating an association between classes
	 * @param e Element of the parent node
	 */
	private void createGeneralization(Element e){
		Element generalization = root.createElement(GENERALIZATION);
		putAttribute(generalization, "xmi.id", "g"+generalizationId);
		generalizationId++;
		e.appendChild(generalization);
		Element generChild = root.createElement(GENERALIZATION+".child");
		generalization.appendChild(generChild);
		Element classChildId = root.createElement(CLASS);
		putAttribute(classChildId, "xmi.idref", ""+(id-1));
		generChild.appendChild(classChildId);
		
		Element generParent = root.createElement(GENERALIZATION+".parent");
		generalization.appendChild(generParent);
		Element classParentId = root.createElement(CLASS);
		generParent.appendChild(classParentId);
		int parentId = getParentId(idList.get(id-1).getParent());
		putAttribute(classParentId, "xmi.idref", ""+parentId);
	}
	
	/***
	 * Method for searching the id of the parent class in the XMI file
	 * @param parent Name of the parent
	 * @return Index of the parent in the XMI file
	 */
	private int getParentId(String parent){
		for(int i = 0; i < idList.size(); i++){
			if(idList.get(i).getName().equalsIgnoreCase(parent))
				return i;
		}
		return -1;
	}
	/***
	 * Method for adding an attribute with a value to a xml tag
	 * @param e The node of the tree we want to add an attribute
	 * @param name Name of the attribute
	 * @param value Value for the attribute
	 */
	private void putAttribute(Element e, String name, String value){
		Attr attr = root.createAttribute(name);
		attr.setValue(value);
		e.setAttributeNode(attr);
	}
}